use std::{
    fs,
    path::{Path, PathBuf},
};

use anyhow::{Result, anyhow};
use counter::FunctionCallCounter;

use crate::{
    data::Command,
    error::InvalidInstructionError,
    writer::{AsmWriter, asm::AsmGenerator},
};

pub(crate) mod counter;

#[derive(Debug)]
pub(crate) struct Translator {
    /// The name of the input file.
    vm_file_name: String,
    /// The path to the input file.
    input_path: PathBuf,
    /// The path to the output file.
    output_path: PathBuf,
}

impl Translator {
    /// translate the vm file or path to a single asm file
    pub(crate) fn translate<P: AsRef<Path>>(vm_file: P, append_bootstrap: bool) -> Result<String> {
        let vm_file = vm_file.as_ref();
        let mut counter = FunctionCallCounter::new();
        let (mut asm_code, output_path) =
            if vm_file.is_dir() {
                // all the vm file path
                let mut vm_file_paths: Vec<_> = vm_file
                    .read_dir()?
                    .flatten()
                    .filter_map(|item| {
                        if item
                            .path()
                            .extension()
                            .unwrap_or_default()
                            .to_string_lossy()
                            == "vm"
                        {
                            Some(item.path())
                        } else {
                            None
                        }
                    })
                    .collect();
                let mut code = Vec::new();
                if let Some(index) = vm_file_paths.iter().position(|item| {
                    item.file_stem().unwrap_or_default().to_string_lossy() == "Sys"
                }) {
                    let sys_file = vm_file_paths.swap_remove(index);
                    code.push(Self::translate_file(&sys_file, &mut counter)?.0)
                }
                if let Some(index) = vm_file_paths.iter().position(|item| {
                    item.file_stem().unwrap_or_default().to_string_lossy() == "Main"
                }) {
                    let main_file = vm_file_paths.swap_remove(index);
                    code.push(Self::translate_file(&main_file, &mut counter)?.0)
                }
                code.extend(
                    vm_file_paths
                        .iter()
                        .flat_map(|file_path| Self::translate_file(file_path, &mut counter))
                        .map(|(asm_code, _)| asm_code),
                );
                let asm_code = code.join("\n");
                let mut out_path = vm_file.join(vm_file.file_name().unwrap_or_default());
                out_path.set_extension("asm");
                (asm_code, out_path)
            } else {
                Self::translate_file(vm_file, &mut counter)?
            };
        if append_bootstrap {
            asm_code.insert_str(0, AsmGenerator::asm_bootstrap_code());
        }
        fs::write(&output_path, asm_code)?;
        Ok(output_path.to_str().unwrap_or_default().to_string())
    }
    /// translate the vm file to asm file
    pub(crate) fn translate_file(
        vm_file: &Path,
        counter: &mut FunctionCallCounter,
    ) -> Result<(String, PathBuf)> {
        let translate = Self::new(vm_file)?;
        let commands = translate.commands()?;
        let asm_code =
            AsmWriter::new(commands, &translate.vm_file_name, counter).tranlate_to_asm()?;
        Ok((asm_code, translate.output_path))
    }

    /// read the vm file and parse to the commands
    fn new<P: AsRef<Path>>(vm_file: P) -> Result<Self> {
        let vm_file = vm_file.as_ref();
        let input_path = vm_file.to_path_buf();
        let file_name = input_path
            .file_stem()
            .ok_or(anyhow!("read input file's name fail"))?
            .to_string_lossy()
            .into_owned();
        if input_path.extension().unwrap_or_default().to_string_lossy() != "vm" {
            Err(InvalidInstructionError::FileExt(
                "input file must be a .vm file".into(),
            ))?;
        }
        let mut output_path = vm_file.to_path_buf();
        output_path.set_extension("asm");

        Ok(Self {
            vm_file_name: file_name,
            input_path,
            output_path,
        })
    }

    /// parse the assembly code's all lines to commands
    fn commands(&self) -> Result<Vec<Command>> {
        fs::read_to_string(&self.input_path)?
            .lines()
            .filter_map(|line| {
                let line = line.trim();
                if !line.is_empty() && !line.starts_with("//") {
                    Some(Self::to_command(line))
                } else {
                    None
                }
            })
            .collect::<Vec<_>>()
            .into_iter()
            .collect()
    }
    /// convert a line of assembly code to a command
    fn to_command(line: &str) -> Result<Command> {
        let line = if line.contains("//") {
            line.split_once("//").unwrap().0
        } else {
            line
        };
        Ok(Command::try_from(line.to_string())?)
    }
}
